/**
 * Copyright 2012 ibiblio
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.ibiblio.terasaur.item

import org.ibiblio.terasaur.user.User
import org.ibiblio.terasaur.item.Item
import org.ibiblio.terasaur.bt.Torrent
import org.joda.time.DateTime
import org.ibiblio.terasaur.search.DateHelper
import org.codehaus.groovy.grails.web.metaclass.BindDynamicMethod
import org.springframework.security.access.prepost.PreAuthorize
import org.grails.plugins.springsecurity.service.acl.AclUtilService
import org.springframework.security.acls.domain.BasePermission
import org.springframework.security.access.AccessDeniedException
import org.ibiblio.terasaur.item.PermissionHelper
import org.springframework.validation.FieldError
import org.grails.plugins.sanitizer.MarkupSanitizerResult

class ItemService {
    static transactional = false
    static _generalCollectionId = 1
    boolean _flushTransactions = false // Option for unit testing
    def springSecurityService // injected
    def aclUtilService // injected
    def searchService // injected
    static PermissionHelper permissionHelper = new PermissionHelper()
    def markupSanitizerService // injected

    def downloads(Map listParams) {
        def c = Torrent.createCriteria()
        def results
        if (listParams['canEditItem']) {
            results = c.list(listParams) {
                eq("item", listParams['item'])
                order("name", "asc")
            }
        } else {
            results = c.list(listParams) {
                eq("item", listParams['item'])
                sqlRestriction("published <= NOW()")
                order("name", "asc")
                cache(true)
            }
        }
        return results
    }

    /**
     * Save a new item
     *
     * @param Map params
     * @return Map result
     */
    def save(Map params, User user) {
        Map result
        Item item = new Item()
        boolean success = false

        // throws AccessDeniedException if user is not allowed to add to collection
        _ensureCanAddToCollection(_getCollectionFromParams(params))

        try {
            _setItemPropertiesFromParams(item, params)
            item.owner = user
            Item.withTransaction { status ->
                if (!this._save_transaction(item, user)) {
                    status.setRollbackOnly()
                } else {
                    success = true
                }
            }
            if (success) {
                // We don't want a failure here to result in a transaction rollback above
                searchService.add(item)
            }
        } catch (Exception e) {
            log.error("Item save exception (" + e.class.name + "): " + e.message)
        }

        result = [
            success: success,
            item: item,
        ]
        return result
    }

    protected boolean _save_transaction(Item item, User user) {
        boolean success = false
        if (item.save(insert:true, flush:_flushTransactions)) {
            aclUtilService.addPermission(item, user.username, BasePermission.ADMINISTRATION)
            aclUtilService.addPermission(item, user.username, BasePermission.WRITE)
            success = true
        }
        return success
    }

    /**
     * Update an existing item record
     *
     * @param params
     * @param item
     * @return
     */
    @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#item, write)")
    public boolean update(Item item, Map params) {
        boolean success = false

        // throws AccessDeniedException if user is not allowed to add to collection
        _ensureCanAddToCollection(_getCollectionFromParams(params))

        try {
            // Ordering here is important due odd behavior where setting
            // properties wipes any existing errors from a domain object.
            _setItemPropertiesFromParams(item, params)
            _update_checkVersion(item, params)

            if (!item.hasErrors() && item.save(flush:_flushTransactions)) {
                success = true
                searchService.add(item)
            }
        } catch (Exception e) {
            log.error("Item update exception (" + e.class.name + "): " + e.message)
        }

        return success
    }

    private void _update_checkVersion(Item item, Map params) {
        if (params.version) {
            Long version = Long.parseLong(params.version)
            if (item.version > version) {
                item.version = version
                item.errors.rejectValue('version', 'default.optimistic.locking.failure',
                                        (Object[])[Item], '')
                throw new ItemServiceException()
            }
        }
    }

    /**
     * Helpers
     */
    private Collection _getCollectionFromParams(params) {
        Collection collection = null
        if (params.collection) {
            collection = Collection.get(params.collection)
        }
        return collection
    }

    private void _ensureCanAddToCollection(Collection collection) {
        boolean canAdd = false
        try {
            if (collection) {
                if (permissionHelper.isAdmin(springSecurityService.currentUser)) {
                    canAdd = true
                } else if (collection.id == _generalCollectionId) {
                    canAdd = true
                } else {
                    canAdd = aclUtilService.hasPermission(springSecurityService.authentication, collection, BasePermission.WRITE)
                }
            }
        } catch (Exception e) {
            log.error(e)
        }
        if (!canAdd) {
            throw new AccessDeniedException('No permission to add item to collection')
        }
    }

    private void _setItemPropertiesFromParams(Item item, Map params) {
        BindDynamicMethod bind = new BindDynamicMethod()
        def args = [ item, params, [exclude:['title', 'description', 'published', 'collection', 'version', 'deleted']] ]
        bind.invoke(item, 'bindData', (Object[])args)

        if (params.title != null) {
            _addSanitizedField('title', item, params.title)
        }
        if (params.description != null) {
            _addSanitizedField('description', item, params.description)
        }
        if (params.published) {
            try {
                item.published = DateHelper.publishedStringToDateTime(params.published)
            } catch (IllegalArgumentException e) {
                def error = new FieldError('Item', 'published', params.published, false, ['item.date.validator.invalid'] as String[], null, '')
                item.errors.addError(error)
                //throw new ItemServiceException('Invalid item published date string')
            }
        }
        if (params.deleted) {
            if (params.deleted == '1') {
                item.deleted = true
            } else if (params.deleted == '0') {
                item.deleted = false
            }
        }
        if (params.collection) {
            item.collection = Collection.get(Long.parseLong(params.collection))
        }
    }

    private String _addSanitizedField(String fieldName, Item item, String input) {
        MarkupSanitizerResult result = markupSanitizerService.sanitize(input)
        item.setProperty(fieldName, result.cleanString)
        if (result.isInvalidMarkup()) {
            def errorMsg = result.errorMessages.join('. ')
            def error = new FieldError('Item', fieldName, input, false, null, null, errorMsg)
            item.errors.addError(error)
        }
    }

    class ItemServiceException extends Exception {
        public ItemServiceException() {
            super()
        }
        public ItemServiceException(String msg) {
            super(msg)
        }
    }
}
